package io.litchi.starter.oss.impl;

import io.litchi.starter.oss.LitchiOssClient;
import io.litchi.starter.oss.dto.CompletedPartDto;
import io.litchi.starter.oss.dto.InitiateUploadResponse;
import io.litchi.starter.oss.dto.UploadPartResponse;
import io.litchi.starter.oss.exception.LitchiOssException;
import io.litchi.starter.oss.properties.LitchiOssProperties;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;


import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;

/**
 * oss客户端
 *
 * @author helang
 * @since 2025-09-19
 */
@Slf4j
@AllArgsConstructor
public class LitchiOssClientImpl implements LitchiOssClient {
    private final S3Client s3Client;
    private final S3Presigner presigner;
    private final LitchiOssProperties properties;
    private String bucketName;

    public LitchiOssClientImpl(LitchiOssProperties properties) {
        this.properties = properties;
        this.bucketName = properties.getBucket();

        // 构建客户端
        this.s3Client = S3Client.builder()
                .endpointOverride(URI.create(properties.getEndpoint()))
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())
                ))
                .region(Region.of(properties.getRegion()))
                .forcePathStyle(properties.isPathStyleAccess())
                .serviceConfiguration(S3Configuration.builder().build())
                .build();

        // 构建签名器（用于生成外链）
        this.presigner = S3Presigner.builder()
                .endpointOverride(URI.create(properties.getEndpoint()))
                .credentialsProvider(StaticCredentialsProvider.create(
                        AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())
                ))
                .region(Region.of(properties.getRegion()))
                .build();
    }

    /**
     * 继承oss暂时不支持公开访问
     * @param bucketName 名称
     */
    @Override
    public void createBucketIfNotExists(String bucketName) {

        try {
            // 直接尝试创建桶
            CreateBucketRequest createBucketRequest = CreateBucketRequest.builder()
                    .bucket(bucketName)
                    // .createBucketConfiguration(...) // 如果不是 us-east-1，需要设置 region
                    .build();
            s3Client.createBucket(createBucketRequest);


        } catch (S3Exception e) {
            if (e.statusCode() == 409 && "BucketAlreadyExists".equals(e.awsErrorDetails().errorCode())) {
                throw new LitchiOssException("bucket is exists",e.statusCode());
            } else if (e.statusCode() == 200) {
                // 某些 S3 兼容服务（如 MinIO）在桶已存在时返回 200
            } else {
                throw new LitchiOssException("Failed to create bucket: " + bucketName, e.statusCode());
            }
        } catch (SdkException e) {
            throw new LitchiOssException("SDK error: " + bucketName);
        }
    }


    @Override
    public String upload(String key, InputStream inputStream, long size, String contentType) {
        PutObjectRequest request = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .contentType(contentType)
                .build();
        s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
        return key;
    }

    @Override
    public String upload(String key, InputStream inputStream, long size, String contentType, String newBucketName) {
        this.createBucketIfNotExists(bucketName);
        PutObjectRequest request = PutObjectRequest.builder()
                .bucket(newBucketName)
                .key(key)
                .contentType(contentType)
                .build();

        s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
        return key;
    }

    @Override
    public InputStream download(String key) {
        GetObjectRequest request = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();

        return s3Client.getObject(request);
    }

    @Override
    public void delete(String key) {
        DeleteObjectRequest request = DeleteObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();
        s3Client.deleteObject(request);
    }

    @Override
    public String getTemporaryUrl(String key, int expireSeconds) {
        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofSeconds(expireSeconds))
                .getObjectRequest(r -> r.bucket(bucketName).key(key))
                .build();

        PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(presignRequest);
        return presignedRequest.url().toString();
    }

    @Override
    public String getPublicUrl(String key) {
        String endpoint = properties.getEndpoint();
        if (!endpoint.startsWith("http")) {
            endpoint = "https://" + endpoint;
        }
        // 判断是否使用 path-style（MinIO 常用）
        if (properties.isPathStyleAccess()) {
            return endpoint + "/" + bucketName + "/" + encodeKey(key);
        } else {
            // virtual-hosted style: https://bucket.endpoint/key
            return endpoint.replace("https://", "https://" + bucketName + ".") + "/" + encodeKey(key);
        }
    }

    /**
     *
     * @param fileName    文件名称
     * @param contentType 文件类型
     * @return 初始化信息
     */
    @Override
    public InitiateUploadResponse initiateMultipartUpload(String fileName, String contentType) {
        String key = properties.getBigFilePrefix() + fileName;

        CreateMultipartUploadRequest request = CreateMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .contentType(contentType)
                .build();

        CreateMultipartUploadResponse response = s3Client.createMultipartUpload(request);


        return new InitiateUploadResponse(
                response.uploadId(),
                bucketName,
                key
        );
    }

    /**
     * 上传分片
     *
     * @param uploadId   uploadId
     * @param key        key
     * @param partNumber 当前分片
     * @param data       数据
     * @return UploadPartResponse
     */
    public UploadPartResponse uploadPart(String uploadId, String key, int partNumber, byte[] data) {
        UploadPartRequest request = UploadPartRequest.builder()
                .bucket(bucketName)
                .key(key)
                .uploadId(uploadId)
                .partNumber(partNumber)
                .build();

        software.amazon.awssdk.services.s3.model.UploadPartResponse response = s3Client.uploadPart(request, RequestBody.fromBytes(data));

        return new UploadPartResponse(partNumber, response.eTag());
    }

    /**
     * 查询当前上传 用于断点上传
     *
     * @param uploadId uploadId
     * @param key      key
     * @return UploadPartResponse
     */
    public List<UploadPartResponse> listUploadedParts(String uploadId, String key) {
        ListPartsRequest listRequest = ListPartsRequest.builder()
                .bucket(bucketName)
                .key(key)
                .uploadId(uploadId)
                .build();

        ListPartsResponse listResponse = s3Client.listParts(listRequest);

        return listResponse.parts().stream()
                .map(part -> new UploadPartResponse(part.partNumber(), part.eTag()))
                .collect(Collectors.toList());
    }

    /**
     * 合并
     *
     * @param uploadId uploadId
     * @param key      key
     * @param partDtos partDtos
     * @return String
     */
    public String completeMultipartUpload(String uploadId, String key, List<CompletedPartDto> partDtos) {
        List<CompletedPart> parts = partDtos.stream()
                .map(dto -> CompletedPart.builder()
                        .partNumber(dto.getPartNumber())
                        .eTag(dto.getETag())
                        .build())
                .collect(Collectors.toList());

        CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                .parts(parts)
                .build();

        CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .uploadId(uploadId)
                .multipartUpload(completedMultipartUpload)
                .build();

        CompleteMultipartUploadResponse response = s3Client.completeMultipartUpload(completeRequest);
        return response.location(); // 返回文件在 S3 的 URL
    }

    // 5. 中止上传（清理）
    public void abortMultipartUpload(String uploadId, String key) {
        AbortMultipartUploadRequest request = AbortMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .uploadId(uploadId)
                .build();
        s3Client.abortMultipartUpload(request);
    }

    // 简单 URL 编码 key
    private String encodeKey(String key) {
        try {
            return URLEncoder.encode(key, StandardCharsets.UTF_8).replace("+", "%20");
        } catch (Exception e) {
            return key; // 失败则原样返回
        }
    }


    // 可选：关闭资源
    public void close() {
        s3Client.close();
        presigner.close();
    }
}